package joe;

import java.util.Vector;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.tinylog.Logger;
import org.tinylog.provider.ProviderRegistry;

public final class Engine {
	private static final Vector<@NotNull Runnable> shutdownHooks = new Vector<>();
	private static @Nullable Engine curEngine; // maybe ThreadLocal?

	static {
		System.getProperties().putIfAbsent("tinylog.configuration", "./tinylog.properties");
		// System.getProperties().putIfAbsent("tinylog.writerConsole.level", "off");

		Runtime.getRuntime().addShutdownHook(new Thread(() -> {
			try {
				Logger.info("Engine: shutdown begin");
				for (var r : shutdownHooks) {
					try {
						r.run();
					} catch (Throwable e) { // logger.error
						Logger.error(e, "Engine: shutdownHook exception");
					}
				}
				shutdownHooks.clear();
			} catch (Throwable e) { // logger.error
				Logger.error(e, "Engine: shutdown fatal exception");
			} finally {
				Logger.info("Engine: shutdown end");
				try {
					ProviderRegistry.getLoggingProvider().shutdown();
				} catch (InterruptedException e) {
					//noinspection CallToPrintStackTrace
					e.printStackTrace();
				}
			}
		}, "EngineShutdownThread"));

		Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
			//noinspection CallToPrintStackTrace
			e.printStackTrace();
			Logger.error(e, "{}: uncaught exception", t);
		});
	}

	public static @NotNull Vector<@NotNull Runnable> getShutdownHooks() {
		return shutdownHooks;
	}

	public static @NotNull Engine current() {
		var e = curEngine;
		return e != null ? e : new Engine();
	}

	public static final class SceneRoot extends SceneNode {
		private SceneRoot() {
			super("ROOT");
		}

		@Override
		protected void onAdded() {
			unlink();
			throw new UnsupportedOperationException("link root node");
		}

		private void tryUpdateTransform() {
			int d = getDirty();
			if (d == 1)
				updateModelMat();
			else if (d == 2)
				updateModelMat(null);
		}
	}

	public static final int TMP_SIZE = 64 * 1024; // must be >=64k and aligned by 4k
	public static final int MAX_QUADS_IN_MESH = 0x10000 / 4; // 16384 for 2-byte index

	{
		curEngine = this;
	}

	private final @NotNull Thread ownerThread = Thread.currentThread();
	private final MemBuf tmpBuf = new MemBuf(TMP_SIZE, 1);
	private final MemPool memPool = new MemPool();
	private final Cleaner cleaner = new Cleaner();
	private final ResourceManager resMgr = new ResourceManager();
	private final ConcurrentLinkedQueue<Runnable> events = new ConcurrentLinkedQueue<>();
	final Mat4 tmpS = new Mat4().identity();
	final Mat4 tmpRz = new Mat4().identity();
	final Mat4 tmpRx = new Mat4().identity();
	final Mat4 tmpRy = new Mat4().identity();
	private Render render;
	private Input input;
	private Audio audio;
	private Render.VertexIndex boxVertexIndex;   // shared box   index buffer, order: near,far,left,right,bottom,top
	private Render.VertexIndex quadsVertexIndex; // shared quads index buffer, order: 0,2,1,1,2,3
	private SceneRoot sceneRoot;
	private @Nullable SceneCamera curCamera;
	private int fps;
	private volatile boolean inited;

	private Engine() {
	}

	public @NotNull Cleaner getCleaner() {
		return cleaner;
	}

	public @NotNull MemBuf getTmpBuf() {
		return tmpBuf;
	}

	@NotNull MemPool getMemPool() {
		return memPool;
	}

	public @NotNull ResourceManager getResourceManager() {
		return resMgr;
	}

	public @NotNull Render getRender() {
		return render;
	}

	public @NotNull Input getInput() {
		return input;
	}

	public @NotNull Audio getAudio() {
		return audio;
	}

	public @NotNull Render.VertexIndex getBoxVertexIndex() {
		return boxVertexIndex;
	}

	public @NotNull Render.VertexIndex getQuadsVertexIndex() {
		return quadsVertexIndex;
	}

	public @NotNull SceneNode getSceneRoot() {
		return sceneRoot;
	}

	public @Nullable SceneCamera getCurCamera() {
		return curCamera;
	}

	@Nullable SceneCamera setCurCamera(@Nullable SceneCamera camera) {
		var oldCamera = curCamera;
		curCamera = camera;
		return oldCamera;
	}

	public int getFps() {
		return fps;
	}

	public boolean isInited() {
		return inited;
	}

	// can be invoked in any thread
	public void postEvent(@NotNull Runnable r) {
		if (inited)
			events.offer(r);
	}

	public void init(@NotNull Render render, @NotNull Render.InitParams params,
					 @NotNull Input input, @NotNull Audio audio) {
		if (ownerThread != Thread.currentThread())
			throw new IllegalStateException("Engine: can not init in another thread");
		if (inited)
			throw new IllegalStateException("Engine: already inited");
		Logger.info("Engine: init begin");
		try {
			this.render = render;
			this.input = input;
			this.audio = audio;
			render.init(params);
			input.init();
			audio.init();

			var tmpPtr = tmpBuf.ptr();
			//@formatter:off
			UnsafeUtil.putShort(tmpPtr     , 0, 1, 2, 2, 1, 3); // near    6----7
			UnsafeUtil.putShort(tmpPtr + 12, 5, 4, 7, 7, 4, 6); // far    2----3|
			UnsafeUtil.putShort(tmpPtr + 24, 4, 0, 6, 6, 0, 2); // left   ||   ||
			UnsafeUtil.putShort(tmpPtr + 36, 1, 5, 3, 3, 5, 7); // right  |4---|5
			UnsafeUtil.putShort(tmpPtr + 48, 4, 5, 0, 0, 5, 1); // bottom 0----1
			UnsafeUtil.putShort(tmpPtr + 60, 2, 3, 6, 6, 3, 7); // top
			//@formatter:on
			boxVertexIndex = render.loadVertexIndex(2, tmpPtr, 6 * 6 * 2); // 6 faces; 2 triangles = 6 vertexes; 2-byte index

			var tmpIndexBuf = UnsafeUtil.allocateMemory(MAX_QUADS_IN_MESH * 6 * 2); // 2 triangles = 6 vertexes; 2-byte index
			try {
				for (int i = 0, p4 = 0, p12 = 0; i < MAX_QUADS_IN_MESH; i++, p4 += 4, p12 += 12)
					UnsafeUtil.putShort(tmpIndexBuf + p12, p4, p4 + 2, p4 + 1, p4 + 1, p4 + 2, p4 + 3);
				quadsVertexIndex = render.loadVertexIndex(2, tmpIndexBuf, MAX_QUADS_IN_MESH * 6 * 2);
			} finally {
				UnsafeUtil.freeMemory(tmpIndexBuf);
			}

			sceneRoot = new SceneRoot();
			Logger.info("Engine: init end");
			inited = true;
		} catch (Exception e) { // logger.error
			Logger.error(e, "Engine: init exception");
		}
	}

	public void run() {
		if (ownerThread != Thread.currentThread())
			throw new IllegalStateException("Engine: can not run in another thread");
		if (!inited || render == null || input == null || audio == null || sceneRoot == null)
			throw new IllegalStateException("Engine: not inited");
		var sleepMs = 1L;
		var frameTimeBegin = System.nanoTime();
		int frameCount = 0;
		while (render.isRunning()) {
			try {
				render.beginRender();
				if (sceneRoot.isEnabled())
					sceneRoot.onRender(render);
				render.endRender();
				resMgr.poll();
				cleaner.poll();
				render.handleEvent();
				input.poll();
				audio.poll();
				if (sceneRoot.isEnabled())
					sceneRoot.onUpdate();
				for (Runnable r; (r = events.poll()) != null; )
					r.run();
				sceneRoot.tryUpdateTransform();
				render.present();
				frameCount++;
				var t = System.nanoTime();
				if (t - frameTimeBegin >= 1_000_000_000) {
					fps = frameCount;
					frameTimeBegin = t;
					frameCount = 0;
				}
				sleepMs = 1;
			} catch (Exception e) { // logger.error
				Logger.error(e, "Engine: loop exception");
				try {
					//noinspection BusyWait
					Thread.sleep(sleepMs);
					sleepMs = Math.min(sleepMs * 2, 1000L);
				} catch (InterruptedException _) {
				}
			}
		}
	}

	public void destroy() {
		if (ownerThread != Thread.currentThread())
			throw new IllegalStateException("Engine: can not destroy in another thread");
		inited = false;
		Logger.info("Engine: destroy begin");
		try {
			curCamera = null;
			sceneRoot = null;
			quadsVertexIndex = null;
			boxVertexIndex = null;
			resMgr.reset();
			events.clear();
			System.gc();
			Thread.sleep(1000);
			resMgr.poll();
			cleaner.poll();
			if (audio != null) {
				audio.destroy();
				audio = null;
			}
			if (input != null) {
				input.destroy();
				input = null;
			}
			if (render != null) {
				render.destroy();
				render = null;
			}
			Logger.info("Engine: destroy end");
		} catch (Exception e) { // logger.error
			Logger.error(e, "Engine: destroy exception");
		} finally {
			curEngine = null;
		}
	}
}
